#include "TextureAtlas.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "Exception.h"
#include "Vertex.h"
#include "tinyxml2.h"
#include "IFileSystem.h"
#include "RenderOperation.h"
#include "IContext.h"
#include "ResourceManager.h"
#include "StringUtils.h"

NS_B2D_BEGIN

B2D_RESOURCE_IMPL(TextureAtlas, BaseResource)
	
TextureAtlas::TextureAtlas(void)
	: mBuffersDirty(false)
	, mVertexBuffer(nullptr)
	, mIndexBuffer(nullptr)
{

}

TextureAtlas::~TextureAtlas(void)
{
	// Release resources
	SafeDelete(mIndexBuffer);
	SafeDelete(mVertexBuffer);
}

void TextureAtlas::initWithTexture(const TexturePtr& texture)
{
	switchTexture(texture);
	if(mItems.size() > 0)
	{
		mItems.clear();
		mBuffersDirty = true;
	}
}

void TextureAtlas::switchTexture(const TexturePtr& texture)
{
	mTextures[0] = texture;
}

const TexturePtr& TextureAtlas::getTexture(void) const
{
	return mTextures[0];
}

size_t TextureAtlas::addItem(const string& name, const Rectu& region, const vec2& origin)
{
	Item it;
	it.name = name;
	it.pixelRegion = region;
	it.origin = origin;
	mItems.push_back(std::move(it));

	
	mBuffersDirty = true;
	return mItems.size() -1;
}

void TextureAtlas::generateBuffers(void)
{
	if(!mBuffersDirty) return;
	

	// Check valid texture
	TexturePtr tex = mTextures[0];
	if(!tex)
	{
		B2D_THROW(Exception::NULL_ALRGUMENT_EXCEPTION,
			"Can not generate atlas with null texture.");
	}
	float invTexWidth = 1.0f / (float)tex->getWidth();
	float invTexHeight = 1.0f / (float)tex->getHeight();

	// Every item requires 2 triangles and thus 4 vertices
	// and 6 indices.
	size_t count = mItems.size();
	size_t indexCount = count * 6;
	size_t vertCount = count * 4;
	size_t vertSize = sizeof(PTVertex);
	VertexFormat vertFormat = PTVertex::Format;

	// Create buffers (if not created yet) and initialise
	IGraphicsDevice& g = *GetContext().getInstance<IGraphicsDevice>();
	if(mVertexBuffer == nullptr)
		mVertexBuffer = g.createVertexBuffer();
	if(mIndexBuffer == nullptr)
		mIndexBuffer = g.createIndexBuffer();
	mVertexBuffer->resize(vertCount, vertFormat, BufferUsage::STATIC_DRAW);
	mIndexBuffer->resize(indexCount, IndexSize::INT16_INDEX, BufferUsage::STATIC_DRAW);

	PTVertex* vertices = nullptr;
	int16* indices = nullptr;
	GPUBuffer::lock_guard vertexLock(*mVertexBuffer, LockMode::WRITE_ONLY_DISCARD, (void**)&vertices, 0, mVertexBuffer->getBufferSize());
	GPUBuffer::lock_guard indexLock(*mIndexBuffer, LockMode::WRITE_ONLY_DISCARD, (void**)&indices, 0, mIndexBuffer->getBufferSize());

	for(size_t i = 0; i < count; ++i)
	{
		size_t v = i * 4;


		// Wite vertices from this item
		float width = (float)mItems[i].pixelRegion.size.x;
		float height = (float)mItems[i].pixelRegion.size.y;
		float left = (-1.0f * (float)mItems[i].origin.x * width); 
		float bottom = ( (1.0f - (float)mItems[i].origin.y) * height * -1.0f); 
		float right = width + left;
		float top = height + bottom;
		vertices[v].x = vertices[v+1].x = left;
		vertices[v].y = vertices[v+3].y = bottom;
		vertices[v+1].y = vertices[v+2].y = top;
		vertices[v+2].x = vertices[v+3].x = right;

		// Write texture coords
		float s0 = mItems[i].pixelRegion.position.x * invTexWidth;
		float t0 = mItems[i].pixelRegion.position.y * invTexHeight;
		float s1 = s0 + mItems[i].pixelRegion.size.x * invTexWidth;
		float t1 = t0 + mItems[i].pixelRegion.size.y * invTexHeight;
		vertices[v].u = vertices[v+1].u = s0;
		vertices[v].v = vertices[v+3].v = t1;
		vertices[v+1].v = vertices[v+2].v = t0;
		vertices[v+2].u = vertices[v+3].u = s1;

		// Write indices
		static const int16 indicesBase[] = {0,1,2, 0,2,3};
		for(size_t j = 0; j < 6; ++j)
			indices[j] = v + indicesBase[j];
	}

	mBuffersDirty = false;
}

void TextureAtlas::applyTextures(IGraphicsDevice& g) const
{
	for(size_t i = 0; i < TextureUnit::MAX; ++i)
	{
		g.setTextureSlot(static_cast<TextureUnit::Unit>(i), mTextures[i]);
	}
}

void TextureAtlas::render(IGraphicsDevice& g, size_t index) const
{
	applyTextures(g);

	// Draw sprite
	RenderOperation op;
	op.vertexBuffers.push_back(mVertexBuffer);
	op.vertexCount = 6;
	op.vertexOffset = 6 * index;
	op.indexBuffer = mIndexBuffer;
	op.primitive = PrimitiveType::TRIANGLES;
	g._render(op);
}

size_t TextureAtlas::getSizeEstimate(void) const
{
	// TODO SP
	return 0;
}

void TextureAtlas::load(const StreamPtr& stream)
{
	IResourceManager& rm = *GetContext().getInstance<IResourceManager>();

	// Get data
	stream->open(AccessMode::READ);
	size_t dataSize = stream->getSize();
	char* data = new char[dataSize];
	stream->read(data, dataSize);
	stream->close();

	// Create document
	tinyxml2::XMLDocument doc;
	doc.Parse(data, dataSize);

	// Check for correct format
	tinyxml2::XMLElement* root = doc.FirstChildElement("TextureAtlas");
	if( root == nullptr )
	{
		B2D_THROW(Exception::PARSING_EXCEPTION,
			"The resource \""+mName.getString()+"\" is not a texture atlas");
	}

	// Texture
	tinyxml2::XMLElement* xmlTex = root->FirstChildElement("Texture");
	TexturePtr tex = nullptr;
	if( xmlTex != nullptr )
	{
		tex = rm.getResource<ITexture>(xmlTex->Attribute("diffuse"));
	}
	if( tex == nullptr )
	{
		B2D_THROW(Exception::PARSING_EXCEPTION,
			"Could not parse atlas \""+mName.getString()+"\" because the texture was not found");
	}
	mTextures[0] = tex;

	// Get items
	tinyxml2::XMLElement* xmlItem = root->FirstChildElement("Item");
	while( xmlItem != nullptr )
	{
		string itemName = xmlItem->Attribute("name");

		// Get origin
		vec2 itemOrigin;
		if( StringUtils::GetCommaSeparated(xmlItem->Attribute("origin"), 2, &itemOrigin[0]) != 2 )
		{
			B2D_THROW(Exception::PARSING_EXCEPTION,
				"Invalid texture atlas item format, Item.Origin must contain 2 comma separated values");
		}

		// Get rect
		Rectu itemRect;
		if( StringUtils::GetCommaSeparated(xmlItem->Attribute("rect"), 4, &itemRect.position.x) != 4 )
		{
			B2D_THROW(Exception::PARSING_EXCEPTION,
				"Invalid texture atlas item format, Item.Rectf must contain 4 comma separated values");
		}

		// Add and go to next
		addItem(itemName, itemRect, itemOrigin);
		xmlItem = xmlItem->NextSiblingElement("Item");
	}

	generateBuffers();
}

NS_B2D_END